Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

engine: Make execution requests a sidecar, take 2 #591

Merged
merged 21 commits into from
Oct 8, 2024

Conversation

mkalinin
Copy link
Collaborator

@mkalinin mkalinin commented Sep 25, 2024

Alternative to:

Proposal

  • Drop requests from EL block and only keep requestsRoot commitment in the EL block header, originates from engine: Return and accept EL triggered requests as a sidecar #551 and engine: Return and accept EL triggered requests as a sidecar #551 (comment)
  • Obtain and pass requests from getPayload and newPayload as a sidecar using byte sequence representation similar to engine: unify request objects #565
  • Make EL validate requestsHash as a part of blockHash validation, note there is no need to keep requests_hash in the beacon block. CLs that run blockHash validation on their own will still be able to do that by implementing the algo EL uses to compute requestsHash
  • Make EL validate requestsHash in the EL block header against requests obtained from transaction execution
  • Make system smart contracts prepending a type byte to each encoded request record, this results in a smart contract returning a ready to be served request list, i.e. requests_01 = withdrawalContract.sysCall(), requests_02 = consolidationContract.sysCall(); deposit contract will always be an exception
  • Use concatenation to compute the resulting list of all requests instead of RLP, i.e. do executionRequests = requests_00 || requests_01 || requests_02 instead of requests = RLP([requests_00, requests_01, requests_02]). And use the executionRequests in Engine API and in requestsHash computation, the latter can be as simple as requestsHash = executionRequests(requests)

Properties

  • Adding a new request type on EL becomes as simple as specifying a new request smart contract address and that’s it, everything else is by generic computations
  • CL has to decode/encode requests from/to byte sequence representation defined by the EIP-7685, but this is reasonable as the CL is the consumer of the requests and EL just surfaces them
  • Optimistic sync is safe as the blockHash validation ensures that the beacon block contains requests that EL payload is committed to and then, upon executing a block, EL ensures that the same commitment corresponds to the requests obtained from transaction execution. These two steps ensure that either requests in the beacon block are the same as those produced by the transaction execution or the payload is INVALID

ToDo

  • Update EIP-7685 by:
    • removing requests from the block body
    • switching requests list encoding from RLP to concatenation and updating requestsHash computation
  • Update system smart contracts to prepend the type byte to each request

@lucassaldanha
Copy link
Contributor

I love the idea of removing the requests from EL block. We should keep iterating on that.

However, when it comes to the Engine API definition, I think we should consider an approach where we send a list of opaque representations of the requests (like an array of byte arrays), similar to what was proposed here. This approach has the following benefits:

  1. EL does not need to know about the structure of each request type;
  2. We don't need to update the Engine API every time a new request type is added;

The requirements for that approach are:

  1. Contracts that create the requests need to return the requests in the same structure the CL is expecting them (maybe as an encoded object?)
  2. CL has to decode the data and respect the order they appear on the array.

@mkalinin
Copy link
Collaborator Author

After a fruitful discussion with @lucassaldanha I have updated the PR and its description with the design we came up to. The final design we propose stems from removing requests from EL block and passing them as a sidecar to the payload and leverages on the requests encoding proposed in #565

@mkalinin
Copy link
Collaborator Author

A thought after the ACDE call: we can use executionRequests representation as in #577, i.e. make it a list of byte arrays, this should still allow for a perfect generalization on the EL side and should make parsing requests on CL side more convenient

@fjl
Copy link
Collaborator

fjl commented Sep 26, 2024

I think it's better to use the flat DATA encoding because then it matches the input for the requestsHash commitment exactly.

@fjl
Copy link
Collaborator

fjl commented Sep 26, 2024

Here is the update to the system contracts (WIP): lightclient/sys-asm#22

@marioevz
Copy link
Member

marioevz commented Sep 26, 2024

A thought after the ACDE call: we can use executionRequests representation as in #577, i.e. make it a list of byte arrays, this should still allow for a perfect generalization on the EL side and should make parsing requests on CL side more convenient

Just for the sake of my own understanding, we expect to always (in Prague that is) see a list here with three elements (one for each request type) every block and independent of whether there are requests of a given type or not, is that correct?

E.g.

[
  "0x00... 00... 00...",  # Three deposits here
  "0x",  # No withdrawals
  "0x02..."  # One consolidation
]

(So not exactly as #577 because to separate each request into a new array element we would need to parse the contract response)

Edit: I just saw fjl comment about how single flat data is better, I'll wait until this is settled before updating the tests.

src/engine/prague.md Outdated Show resolved Hide resolved
Co-authored-by: Mario Vega <marioevz@gmail.com>
@mkalinin
Copy link
Collaborator Author

(So not exactly as #577 because to separate each request into a new array element we would need to parse the contract response)

EL would not have to parse data to serve request lists separately. Each list is obtained from the corresponding smart contract and then concatenated into a single flat list to compute the requestsHash. We have two options in Engine API: a) serve the concatenated list with all requests b) serve three lists separately, as like in your example.

Obviously, (a) is better from EL perspective, while option (b) isn’t something complicated to do either. So, I would like to hear from CL client devs on that

@tersec
Copy link
Contributor

tersec commented Sep 27, 2024

(So not exactly as #577 because to separate each request into a new array element we would need to parse the contract response)

EL would not have to parse data to serve request lists separately. Each list is obtained from the corresponding smart contract and then concatenated into a single flat list to compute the requestsHash. We have two options in Engine API: a) serve the concatenated list with all requests b) serve three lists separately, as like in your example.

Obviously, (a) is better from EL perspective, while option (b) isn’t something complicated to do either. So, I would like to hear from CL client devs on that

I significantly prefer (b).

The only specific utility of the flat list is for hashing, and that can be done by init(), then update() on each sublist in turn, then finalize. The flat is on its own isn't useful to either the EL or CL.

@marioevz
Copy link
Member

(So not exactly as #577 because to separate each request into a new array element we would need to parse the contract response)

EL would not have to parse data to serve request lists separately. Each list is obtained from the corresponding smart contract and then concatenated into a single flat list to compute the requestsHash. We have two options in Engine API: a) serve the concatenated list with all requests b) serve three lists separately, as like in your example.
Obviously, (a) is better from EL perspective, while option (b) isn’t something complicated to do either. So, I would like to hear from CL client devs on that

I significantly prefer (b).

The only specific utility of the flat list is for hashing, and that can be done by init(), then update() on each sublist in turn, then finalize. The flat is on its own isn't useful to either the EL or CL.

I agree, I'm even less inclined to a single flat byte string now.

Another advantage of three-element list (or N element for future forks) is that you can modulo the length of each element in the list against the length of a single request for the given type and immediately validate it has the correct size.

We could even skip the request-type byte at the beginning of every request this way.

Wdyt @fjl @mkalinin ?

@fjl
Copy link
Collaborator

fjl commented Sep 27, 2024

I think it's better to serve the requests as a single flat byte array because that is also how they will be hashed to create the requestsHash in the block header. For example, if a block has one deposit (D₁), two withdrawal requests (W₁, W₂), and one consolidation request (C₁), the requestsHash commitment can be sha256(D₁ || W₁ || W₂ || C₁).

So I think we should relay the requests to the CL as the byte array D₁ || W₁ || W₂ || C₁ because this makes it trivial to check the hash with no extra encoding step. In order to process the requests, the CL just has to parse them from the byte array, which is trivial because the size of each request type is fixed and known to the CL.

@marioevz
Copy link
Member

marioevz commented Sep 27, 2024

I think it's better to serve the requests as a single flat byte array because that is also how they will be hashed to create the requestsHash in the block header. For example, if a block has one deposit (D₁), two withdrawal requests (W₁, W₂), and one consolidation request (C₁), the requestsHash commitment can be sha256(D₁ || W₁ || W₂ || C₁).

So I think we should relay the requests to the CL as the byte array D₁ || W₁ || W₂ || C₁ because this makes it trivial to check the hash with no extra encoding step. In order to process the requests, the CL just has to parse them from the byte array, which is trivial because the size of each request type is fixed and known to the CL.

I agree that it is a bit simpler to hash on a flat array but having a list of byte arrays you only need to do two byte array concatenations (or N-1 on future forks where N is the number of request types) and you end up with the same information before hashing, because given D = D₁, W = W₁ || W₂ and C = C₁, D || W || C = D₁ || W₁ || W₂ || C₁.

@fjl
Copy link
Collaborator

fjl commented Sep 27, 2024

Yes I understand. But there is another benefit to the flat array, and it has to do with abstraction. At the moment, we are focused on the three request types coming out of three distinct contract invocations. However, this may not always be the case in the future.

We could later decide that the withdrawal request queue logic should be changed and have a more complex behavior. We can't change the contract, but we could just add another system contract then and make it output the same withdrawal events. Or we could have a future system contract that outputs multiple kinds of events.

For this reason, I strongly believe we should go with the flat byte array returning the contracts return data as-is. It removes the need for the EL to know anything about the output, and it allows the CL to be ignorant about the source of the requests.

We should probably also remove the type-ordering requirement in CL validation. The only thing we really need to specify is the system contract call order for the EL.

@pawanjay176
Copy link

pawanjay176 commented Sep 27, 2024

Also prefer (b), but I kinda understand the motivation for a flat list.

If we are going with (b), the spec should be clear on the validity of ordering within the flat list. Should we expect the EL to return an ordered by type flat list?

For e.g. if we receive [D1 , W1 ,D2, C1 , W2 ], should we

  1. set the execution_requests object in the beacon block body as
    deposits: [D1 , D2 ]
    withdrawals: [W1 ,W2]
    consolidations: [C1 ]
  2. return a deserialisation error
  3. Ignore the out of order types and set it as
    deposits: [D1 ]
    withdrawals: [W1 ]
    consolidations: [C1 ]

(3) doesn't make sense to me, I'm unclear if we should do (1) or (2)

@lightclient
Copy link
Member

I updated the PR to send exeucitonRequestsHash instead of full requests, per the decision on the testing call today. PTAL.

Copy link
Member

@marioevz marioevz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

src/engine/prague.md Outdated Show resolved Hide resolved
@lightclient lightclient merged commit 4140e52 into ethereum:main Oct 8, 2024
3 checks passed
yperbasis pushed a commit to erigontech/erigon that referenced this pull request Oct 23, 2024
Requests to be removed from body and put into Engine-API sidecar
Ref: ethereum/execution-apis#591
Issue board: #12106
somnathb1 added a commit to erigontech/erigon that referenced this pull request Oct 23, 2024
Summary of changes
- Remove `Requests` interface in favour of new `FlatRequest` struct
- Add changes for new `RequestHash` calculation that `sha256` digests
the set of flat requests
- Remove `Requests` from block and body related structs and methods
- Set of requests that gets pulled at the `Finalize` stage is now
returned from there, both for execution and block-building

Ref1: ethereum/execution-apis#591
Ref2: ethereum/EIPs#8854
Ref3: ethereum/EIPs#8924


Needs interface change -
erigontech/interfaces#239

(Tasks board - #12106)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.